<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">
    <title>WebGPU Perspective - perspective - Step 4</title>
    <style>
      @import url(resources/webgpu-lesson.css);
html, body {
  margin: 0;       /* remove the default margin          */
  height: 100%;    /* make the html,body fill the page   */
}
canvas {
  display: block;  /* make the canvas act like a block   */
  width: 100%;     /* make the canvas fill its container */
  height: 100%;
}
.fill-container {
  width: 100%;
  height: 100%;
}
.split-horizontal {
  display: flex;
  justify-content: center;
  align-items: stretch;
}
.split-horizontal>* {
  flex: 1 1 auto;
  min-width: 0;
}
:root {
  --bg-color: #fff;
}
@media (prefers-color-scheme: dark) {
  :root {
    --bg-color: #000;
  }
}
canvas {
  background-color: var(--bg-color);
}
    </style>
  </head>
  <body>
    <div class="split-horizontal fill-container">
      <canvas id="main"></canvas>
      <canvas id="alt"></canvas>
    </div>
  </body>
  <script type="module">
// see https://webgpufundamentals.org/webgpu/lessons/webgpu-utils.html#wgpu-matrix
import {mat4} from '../3rdparty/wgpu-matrix.module.js';
import GUI from '../3rdparty/muigui-0.x.module.js';

function createFVertices() {
  const positions = [
    // left column
     -50,  75,  15,
     -20,  75,  15,
     -50, -75,  15,
     -20, -75,  15,

    // top rung
     -20,  75,  15,
      50,  75,  15,
     -20,  45,  15,
      50,  45,  15,

    // middle rung
     -20,  15,  15,
      20,  15,  15,
     -20, -15,  15,
      20, -15,  15,

    // left column back
     -50,  75, -15,
     -20,  75, -15,
     -50, -75, -15,
     -20, -75, -15,

    // top rung back
     -20,  75, -15,
      50,  75, -15,
     -20,  45, -15,
      50,  45, -15,

    // middle rung back
     -20,  15, -15,
      20,  15, -15,
     -20, -15, -15,
      20, -15, -15,
  ];

  const indices = [
     0,  2,  1,    2,  3,  1,   // left column
     4,  6,  5,    6,  7,  5,   // top run
     8, 10,  9,   10, 11,  9,   // middle run

    12, 13, 14,   14, 13, 15,   // left column back
    16, 17, 18,   18, 17, 19,   // top run back
    20, 21, 22,   22, 21, 23,   // middle run back

     0,  5, 12,   12,  5, 17,   // top
     5,  7, 17,   17,  7, 19,   // top rung right
     6, 18,  7,   18, 19,  7,   // top rung bottom
     6,  8, 18,   18,  8, 20,   // between top and middle rung
     8,  9, 20,   20,  9, 21,   // middle rung top
     9, 11, 21,   21, 11, 23,   // middle rung right
    10, 22, 11,   22, 23, 11,   // middle rung bottom
    10,  3, 22,   22,  3, 15,   // stem right
     2, 14,  3,   14, 15,  3,   // bottom
     0, 12,  2,   12, 14,  2,   // left
  ];

  const quadColors = [
      200,  70, 120,  // left column front
      200,  70, 120,  // top rung front
      200,  70, 120,  // middle rung front

       80,  70, 200,  // left column back
       80,  70, 200,  // top rung back
       80,  70, 200,  // middle rung back

       70, 200, 210,  // top
      160, 160, 220,  // top rung right
       90, 130, 110,  // top rung bottom
      200, 200,  70,  // between top and middle rung
      210, 100,  70,  // middle rung top
      210, 160,  70,  // middle rung right
       70, 180, 210,  // middle rung bottom
      100,  70, 210,  // stem right
       76, 210, 100,  // bottom
      140, 210,  80,  // left
  ];

  const numVertices = indices.length;
  const vertexData = new Float32Array(numVertices * 4); // xyz + color
  const colorData = new Uint8Array(vertexData.buffer);

  for (let i = 0; i < indices.length; ++i) {
    const positionNdx = indices[i] * 3;
    const position = positions.slice(positionNdx, positionNdx + 3);
    vertexData.set(position, i * 4);

    const quadNdx = (i / 6 | 0) * 3;
    const color = quadColors.slice(quadNdx, quadNdx + 3);
    colorData.set(color, i * 16 + 12);
    colorData[i * 16 + 15] = 255;
  }

  return {
    vertexData,
    numVertices,
  };
}

function createCameraFrustumVertices() {
  const positions = [
    -1, -1,   0,  // cube vertices
     1, -1,   0,
    -1,  1,   0,
     1,  1,   0,
    -1, -1,   1,
     1, -1,   1,
    -1,  1,   1,
     1,  1,   1,
  ];
  const indices = [
    0, 1, 1, 3, 3, 2, 2, 0, // cube indices
    4, 5, 5, 7, 7, 6, 6, 4,
    0, 4, 1, 5, 3, 7, 2, 6,
  ];
  return {
    vertexData: new Float32Array(positions),
    indexData: new Uint16Array(indices),
  };
}

// create geometry for a camera
function createCameraVertices() {
  // first let's add a cube. It goes from 1 to 3
  // because cameras look down -Z so we want
  // the camera to start at Z = 0.
  // We'll put a cone in front of this cube opening
  // toward -Z
  const positions = [
    -1, -1,  1,  // cube vertices
     1, -1,  1,
    -1,  1,  1,
     1,  1,  1,
    -1, -1,  3,
     1, -1,  3,
    -1,  1,  3,
     1,  1,  3,
     0,  0,  1,  // cone tip
  ];
  const indices = [
    0, 1, 1, 3, 3, 2, 2, 0, // cube indices
    4, 5, 5, 7, 7, 6, 6, 4,
    0, 4, 1, 5, 3, 7, 2, 6,
  ];
  // add cone segments
  const numSegments = 6;
  const coneBaseIndex = positions.length / 3;
  const coneTipIndex =  coneBaseIndex - 1;
  for (let i = 0; i < numSegments; ++i) {
    const u = i / numSegments;
    const angle = u * Math.PI * 2;
    const x = Math.cos(angle);
    const y = Math.sin(angle);
    positions.push(x, y, 0);
    // line from tip to edge
    indices.push(coneTipIndex, coneBaseIndex + i);
    // line from point on edge to next point on edge
    indices.push(coneBaseIndex + i, coneBaseIndex + (i + 1) % numSegments);
  }
  return {
    vertexData: new Float32Array(positions),
    indexData: new Uint16Array(indices),
  };
}

async function main() {
  const adapter = await navigator.gpu?.requestAdapter();
  const device = await adapter?.requestDevice();
  if (!device) {
    fail('need a browser that supports WebGPU');
    return;
  }

  const degToRad = d => d * Math.PI / 180;

  const settings = {
    fieldOfView: degToRad(60),
    camera: [-65, 100, 120],
    target: [0, 0, 0],
    near: 1,
    far: 250,
  };

  const limitOptions = { min: 1, max: 500, minRange: 1, step: 1 };

  const gui = new GUI();
  GUI.setTheme('float');
  gui.onChange(render);
  gui.add(settings, 'fieldOfView', {min: 1, max: 179, converters: GUI.converters.radToDeg});
  GUI.makeMinMaxPair(gui, settings, 'near', 'far', limitOptions);
  gui.add(settings.camera, '0', -300, 300).name('camera.x');
  gui.add(settings.camera, '1', -300, 300).name('camera.y');
  gui.add(settings.camera, '2', -300, 300).name('camera.z');
  gui.add(settings.target, '0', -300, 300).name('target.x');
  gui.add(settings.target, '1', -300, 300).name('target.y');
  gui.add(settings.target, '2', -300, 300).name('target.z');

  const mainCanvas = document.querySelector('#main');
  const altCanvas = document.querySelector('#alt');
  const canvases = new Map([
    [
      mainCanvas,
      {
        clearValue: [0.3, 0.3, 0.3, 1.0],
        drawCamera: false,
        settings,
      },
    ],
    [
      altCanvas,
      {
        clearValue: [0.5, 0.5, 0.5, 1.0],
        drawCamera: true,
        settings: {
          fieldOfView: degToRad(60),
          camera: [300, 400, 700],
          target: [0, 75, 0],
          near: 1,
          far: 2000,
        },
      },
    ],
  ]);

  const presentationFormat = navigator.gpu.getPreferredCanvasFormat();

  // Get a WebGPU context from the canvas and configure it
  canvases.forEach((info, canvas) => {
    const context = canvas.getContext('webgpu');
    context.configure({
      device,
      format: presentationFormat,
    });
    info.context = context;
  });

  const module = device.createShaderModule({
    code: /* wgsl */ `
      struct Uniforms {
        matrix: mat4x4f,
      };

      struct Vertex {
        @location(0) position: vec4f,
        @location(1) color: vec4f,
      };

      struct VSOutput {
        @builtin(position) position: vec4f,
        @location(0) color: vec4f,
      };

      @group(0) @binding(0) var<uniform> uni: Uniforms;

      @vertex fn vs(vert: Vertex) -> VSOutput {
        var vsOut: VSOutput;
        vsOut.position = uni.matrix * vert.position;
        vsOut.color = vert.color;
        return vsOut;
      }

      @fragment fn fs(vsOut: VSOutput) -> @location(0) vec4f {
        return vsOut.color;
      }
    `,
  });

  const bindGroupLayout = device.createBindGroupLayout({
    entries: [
      { binding: 0, visibility: GPUShaderStage.VERTEX, buffer: { } },
    ],
  });

  const layout = device.createPipelineLayout({
    bindGroupLayouts: [
      bindGroupLayout,
    ],
  });

  const pipeline = device.createRenderPipeline({
    label: '2 attributes',
    layout,
    vertex: {
      module,
      buffers: [
        {
          arrayStride: (4) * 4, // (3) floats 4 bytes each + one 4 byte color
          attributes: [
            {shaderLocation: 0, offset: 0, format: 'float32x3'},  // position
            {shaderLocation: 1, offset: 12, format: 'unorm8x4'},  // color
          ],
        },
      ],
    },
    fragment: {
      module,
      targets: [{ format: presentationFormat }],
    },
    primitive: {
      cullMode: 'back',
    },
    depthStencil: {
      depthWriteEnabled: true,
      depthCompare: 'less',
      format: 'depth24plus',
    },
  });

  const linePipeline = device.createRenderPipeline({
    label: '2 attributes line-list',
    layout,
    vertex: {
      module,
      buffers: [
        {
          arrayStride: (3) * 4, // (3) floats 4 bytes each
          attributes: [
            {shaderLocation: 0, offset: 0, format: 'float32x3'},  // position
          ],
        },
        {
          arrayStride: 0,  // repeat
          attributes: [
            {shaderLocation: 1, offset: 0, format: 'unorm8x4'},  // color
          ],
        },
      ],
    },
    fragment: {
      module,
      targets: [{ format: presentationFormat }],
    },
    primitive: {
      topology: 'line-list',
    },
    depthStencil: {
      depthWriteEnabled: true,
      depthCompare: 'less',
      format: 'depth24plus',
    },
  });

  // matrix
  const uniformBufferSize = (16) * 4;
  const uniformBuffer = device.createBuffer({
    label: 'uniforms',
    size: uniformBufferSize,
    usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
  });

  const uniformValues = new Float32Array(uniformBufferSize / 4);

  // offsets to the various uniform values in float32 indices
  const kMatrixOffset = 0;

  const matrixValue = uniformValues.subarray(kMatrixOffset, kMatrixOffset + 16);

  const { vertexData, numVertices } = createFVertices();
  const vertexBuffer = device.createBuffer({
    label: 'vertex buffer vertices',
    size: vertexData.byteLength,
    usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
  });
  device.queue.writeBuffer(vertexBuffer, 0, vertexData);

  const cameraResources = (() => {
    const { vertexData, indexData } = createCameraVertices();
    const vertexBuffer = device.createBuffer({
      label: 'camera vertex buffer',
      size: vertexData.byteLength,
      usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
    });
    device.queue.writeBuffer(vertexBuffer, 0, vertexData);

    // A color we'll pass in as vertex colors but with arrayStride = 0
    // so it will just use the same value for every vertex.
    const colorBuffer = device.createBuffer({
      label: 'camera vertex buffer',
      size: 4,
      usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
    });
    device.queue.writeBuffer(colorBuffer, 0, new Uint8Array([1, 1, 0, 1]));

    const indexBuffer = device.createBuffer({
      label: 'camera index buffer',
      size: indexData.byteLength,
      usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST,
    });
    device.queue.writeBuffer(indexBuffer, 0, indexData);

    const uniformBufferSize = (16) * 4;
    const uniformBuffer = device.createBuffer({
      label: 'uniforms',
      size: uniformBufferSize,
      usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
    });

    const uniformValues = new Float32Array(uniformBufferSize / 4);
    const matrixValue = uniformValues.subarray(kMatrixOffset, kMatrixOffset + 16);

    const bindGroup = device.createBindGroup({
      label: 'bind group for camera',
      layout: bindGroupLayout,
      entries: [
        { binding: 0, resource: { buffer: uniformBuffer }},
      ],
    });


    return {
      vertexBuffer,
      colorBuffer,
      indexBuffer,
      indexFormat: 'uint16',
      numElements: indexData.length,
      uniformBuffer,
      uniformValues,
      matrixValue,
      bindGroup,
    };
  })();

  const frustumResources = (() => {
    const { vertexData, indexData } = createCameraFrustumVertices();
    const vertexBuffer = device.createBuffer({
      label: 'camera vertex buffer',
      size: vertexData.byteLength,
      usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
    });
    device.queue.writeBuffer(vertexBuffer, 0, vertexData);

    // A color we'll pass in as vertex colors but with arrayStride = 0
    // so it will just use the same value for every vertex.
    const colorBuffer = device.createBuffer({
      label: 'camera vertex buffer',
      size: 4,
      usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
    });
    device.queue.writeBuffer(colorBuffer, 0, new Uint8Array([1, 1, 0, 1]));

    const indexBuffer = device.createBuffer({
      label: 'camera index buffer',
      size: indexData.byteLength,
      usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST,
    });
    device.queue.writeBuffer(indexBuffer, 0, indexData);

    const uniformBufferSize = (16) * 4;
    const uniformBuffer = device.createBuffer({
      label: 'uniforms',
      size: uniformBufferSize,
      usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
    });

    const uniformValues = new Float32Array(uniformBufferSize / 4);
    const matrixValue = uniformValues.subarray(kMatrixOffset, kMatrixOffset + 16);

    const bindGroup = device.createBindGroup({
      label: 'bind group for camera',
      layout: bindGroupLayout,
      entries: [
        { binding: 0, resource: { buffer: uniformBuffer }},
      ],
    });


    return {
      vertexBuffer,
      colorBuffer,
      indexBuffer,
      indexFormat: 'uint16',
      numElements: indexData.length,
      uniformBuffer,
      uniformValues,
      matrixValue,
      bindGroup,
    };
  })();

  const bindGroup = device.createBindGroup({
    label: 'bind group for object',
    layout: bindGroupLayout,
    entries: [
      { binding: 0, resource: { buffer: uniformBuffer }},
    ],
  });

  const renderPassDescriptor = {
    label: 'our basic canvas renderPass',
    colorAttachments: [
      {
        // view: <- to be filled out when we render
        loadOp: 'clear',
        storeOp: 'store',
      },
    ],
    depthStencilAttachment: {
      // view: <- to be filled out when we render
      depthClearValue: 1.0,
      depthLoadOp: 'clear',
      depthStoreOp: 'store',
    },
  };

  let depthTexture;

  function render() {
    canvases.forEach((_, canvas) => renderCanvas(canvas));
  }

  function renderCanvas(canvas) {
    const canvasInfo = canvases.get(canvas);
    const {
      context,
      clearValue,
      drawCamera,
      settings,
    } = canvasInfo;

    // Get the current texture from the canvas context and
    // set it as the texture to render to.
    const canvasTexture = context.getCurrentTexture();
    renderPassDescriptor.colorAttachments[0].view = canvasTexture.createView();

    // If we don't have a depth texture OR if its size is different
    // from the canvasTexture when make a new depth texture
    if (!depthTexture ||
        depthTexture.width !== canvasTexture.width ||
        depthTexture.height !== canvasTexture.height) {
      if (depthTexture) {
        depthTexture.destroy();
      }
      depthTexture = device.createTexture({
        size: [canvasTexture.width, canvasTexture.height],
        format: 'depth24plus',
        usage: GPUTextureUsage.RENDER_ATTACHMENT,
      });
    }
    renderPassDescriptor.depthStencilAttachment.view = depthTexture.createView();
    renderPassDescriptor.colorAttachments[0].clearValue = clearValue;

    const encoder = device.createCommandEncoder();
    const pass = encoder.beginRenderPass(renderPassDescriptor);
    pass.setPipeline(pipeline);
    pass.setVertexBuffer(0, vertexBuffer);

    const aspect = canvas.clientWidth / canvas.clientHeight;
    const projection = mat4.perspective(
        settings.fieldOfView,
        aspect,
        settings.near,
        settings.far
    );

    // Compute a view matrix
    const up = [0, 1, 0];
    const viewMatrix = mat4.lookAt(settings.camera, settings.target, up);

    mat4.multiply(projection, viewMatrix, matrixValue);

    canvasInfo.projection = projection;
    canvasInfo.viewMatrix = viewMatrix;

    // upload the uniform values to the uniform buffer
    device.queue.writeBuffer(uniformBuffer, 0, uniformValues);

    pass.setBindGroup(0, bindGroup);
    pass.draw(numVertices);

    if (drawCamera) {
      {
        const {
          indexBuffer,
          indexFormat,
          vertexBuffer,
          colorBuffer,
          numElements,
          uniformBuffer,
          uniformValues,
          bindGroup,
          matrixValue,
        } = cameraResources;

        // use the first's camera's matrix as the matrix to position
        // the camera's representative in the scene
        const {viewMatrix: mainViewMatrix} = canvases.get(mainCanvas);
        const mainCameraMatrix = mat4.inverse(mainViewMatrix);
        mat4.multiply(projection, viewMatrix, matrixValue);
        mat4.multiply(matrixValue, mainCameraMatrix, matrixValue);
        mat4.scale(matrixValue, [20, 20, 20], matrixValue);

        device.queue.writeBuffer(uniformBuffer, 0, uniformValues);

        pass.setPipeline(linePipeline);
        pass.setVertexBuffer(0, vertexBuffer);
        pass.setVertexBuffer(1, colorBuffer);
        pass.setIndexBuffer(indexBuffer, indexFormat);
        pass.setBindGroup(0, bindGroup);
        pass.drawIndexed(numElements);
      }
      {
        const {
          indexBuffer,
          indexFormat,
          vertexBuffer,
          colorBuffer,
          numElements,
          uniformBuffer,
          uniformValues,
          bindGroup,
          matrixValue,
        } = frustumResources;

        // use the first's camera's matrix as the matrix to position
        // the camera's representative in the scene
        const {viewMatrix: mainViewMatrix, projection: mainProjection} = canvases.get(mainCanvas);
        const mainCameraMatrix = mat4.inverse(mainViewMatrix);
        mat4.multiply(projection, viewMatrix, matrixValue);
        mat4.multiply(matrixValue, mainCameraMatrix, matrixValue);
        mat4.multiply(matrixValue, mat4.inverse(mainProjection), matrixValue);

        device.queue.writeBuffer(uniformBuffer, 0, uniformValues);

        pass.setPipeline(linePipeline);
        pass.setVertexBuffer(0, vertexBuffer);
        pass.setVertexBuffer(1, colorBuffer);
        pass.setIndexBuffer(indexBuffer, indexFormat);
        pass.setBindGroup(0, bindGroup);
        pass.drawIndexed(numElements);
      }
    }

    pass.end();

    const commandBuffer = encoder.finish();
    device.queue.submit([commandBuffer]);
  }

  const observer = new ResizeObserver(entries => {
    for (const entry of entries) {
      const canvas = entry.target;
      const width = entry.contentBoxSize[0].inlineSize;
      const height = entry.contentBoxSize[0].blockSize;
      canvas.width = Math.max(1, Math.min(width, device.limits.maxTextureDimension2D));
      canvas.height = Math.max(1, Math.min(height, device.limits.maxTextureDimension2D));
      // re-render
      renderCanvas(canvas);
    }
  });
  canvases.forEach((_, canvas) => {
    observer.observe(canvas);
  });
}

function fail(msg) {
  alert(msg);
}

main();
  </script>
</html>
